Skip to main content
Version: 12.10.0

Code Structure

Overview

This project adopts the MVVM (Model-View-ViewModel) Clean Architecture pattern with Jetpack Compose for building the UI. It leverages Dagger Hilt for dependency injection, uses mapper classes for data transformation, and employs a navigation component for in-app routing. The architecture promotes a clear separation of concerns, modularity, and testability.

Project Structure

The project is structured into several key directories, each containing related components for each layer of the architecture:

Data Layer

  • repository: Mention how the repository interfaces with data sources (e.g., local database, remote API) and handles data operations like caching.
  • model: Specify naming conventions for request and response data classes and the importance of using kotlinx.serialization for JSON parsing.
  • mapper: Explain the scenarios where mappers are particularly useful, such as when transforming network response models to domain models or vice versa.

di

  • Elaborate on the types of dependencies provided (e.g., ViewModels, Retrofit services, database instances) and the scope of the dependencies (e.g., application-wide, per-activity).
  • Detail how the navigation graph is organized (e.g., nested graphs for complex flows) and how arguments are passed between composables/screens.

ui

  • model: Clarify the role of UI models in representing the state of the screen and how they differ from data layer models.
  • viewmodel: Discuss the ViewModel's lifecycle and how it survives configuration changes to preserve UI state.
  • view: Mention the use of theming, styling, and animations within composable functions to enhance the user experience.

Events

Events are used to represent one-time actions or occurrences that the UI should react to. These can include navigation events, showing toasts, or triggering dialogs. In the ViewModel, events are typically wrapped in a class like Event to ensure they are consumed only once, even if the UI re-composes due to a configuration change.

Example:

class Screen1ViewModel : ViewModel() {
private val _events = MutableLiveData<Event<Screen1Event>>()
val events: LiveData<Event<Screen1Event>> = _events

fun onSomeAction() {
// Trigger an event that the UI will consume
_events.value = Event(Screen1Event.NavigateToScreen2)
}
}

sealed class Screen1Event {
object NavigateToScreen2 : Screen1Event()
}

UI States

UI states represent the current state of the UI, including the data being displayed and any UI-related flags (e.g., loading, error states). The ViewModel exposes these states to the UI through a LiveData or StateFlow, and the UI reacts by recomposing the relevant composables when the state changes.

Example:

class Screen1ViewModel : ViewModel() {
private val _uiState = MutableLiveData<Screen1UiState>()
val uiState: LiveData<Screen1UiState> = _uiState

init {
_uiState.value = Screen1UiState(loading = true)
// Load data and update the UI state accordingly
}
}

data class Screen1UiState(
val loading: Boolean = false,
val data: List<DataItem>? = null,
val error: String? = null
)

Unidirectional State Flow (UDF)

UDF is a design pattern where the data flows in one direction, from the ViewModel to the UI. The ViewModel exposes states and events, and the UI observes these and reacts accordingly. This pattern simplifies the data flow, making it easier to understand and debug.

UDF is a design pattern where data flows in a single direction, creating a clear and predictable data lifecycle. In the context of Android's MVVM architecture, this means that the ViewModel exposes state and events, and the UI layer observes and reacts to these changes. The ViewModel does not directly manipulate the UI; instead, it updates the state or triggers events that the UI layer observes.

This pattern has several benefits:

  • Predictability: Since data flows in one direction, it's easier to track where data changes originate and how they propagate through the system.
  • Simplicity: Each component has a clear role. ViewModels manage state and logic, while the UI layer is purely declarative.
  • Maintainability: With a clear separation of concerns, it's easier to maintain and update the codebase. Changes in one layer have minimal impact on others.
  • Testability: Each layer can be tested independently. ViewModels can be unit tested without concern for UI implementation details.

Practical Implementation of UDF

In practice, implementing UDF often involves the use of LiveData or StateFlow in the ViewModel. The UI layer observes these streams of data, and Compose functions reactively update when the observed data changes. The ViewModel updates the LiveData or StateFlow in response to user actions or data changes, and the UI reflects these updates.

For example, consider a login screen where the ViewModel exposes a LiveData<LoginUiState>. The LoginUiState might include properties like isLoading, errorMessage, and isLoginSuccessful. The Composable function observing this LiveData would display a loading spinner, an error message, or navigate to the next screen based on the current state.

Example:

@Composable
fun Screen1(viewModel: Screen1ViewModel) {
val uiState by viewModel.uiState.observeAsState()
val events by viewModel.events.observeAsState()

// React to UI state changes
if (uiState.loading) {
CircularProgressIndicator()
} else {
uiState.data?.let { data ->
DataList(data)
}
}

// Handle one-time events
events?.getContentIfNotHandled()?.let { event ->
when (event) {
is Screen1Event.NavigateToScreen2 -> {
// Navigate to Screen2
}
}
}
}

In this structure, the ui directory contains the model, viewmodel, and view subdirectories for each screen. The model subdirectory holds UI model data classes that represent the UI state. The viewmodel subdirectory contains the ViewModel classes that manage the UI logic and state. The view subdirectory includes the composable functions that define the UI.

PluginName
├── data
│ ├── mapper
│ │ └── (various mapper classes)
│ ├── model
│ │ └── (various request and response data classes)
│ └── repository
│ ├── Repository.kt
│ └── RepositoryImpl.kt
├── di
│ └── (Dagger Hilt modules)
├── navigation
│ └── (navigation graph and routing logic)
└── ui
├── screen1
│ ├── model
│ │ └── (UI model data classes)
│ ├── viewmodel
│ │ └── Screen1ViewModel.kt
│ └── view
│ └── Screen1Composable.kt
├── screen2
│ ├── model
│ │ └── (UI model data classes)
│ ├── viewmodel
│ │ └── Screen2ViewModel.kt
│ └── view
│ └── Screen2Composable.kt
└── (additional screens following the same structure)

This folder tree diagram represents the structure of the project, following the MVVM Clean Architecture pattern with Jetpack Compose. Each directory corresponds to a specific layer or component within the architecture, providing a visual representation of the project's organization.